21.3 执行

在探究执行方式之前,先得搞清楚这个执行队列是怎么回事。

析构函数相关信息从special解包后,被重新打包成finalizer,然后被存储到一个由数组封装而成的finblock容器(块)里。多个finblock串成链表,形成队列。看上去很像前文垃圾回收器里的gcWork高性能缓存队列的做法。

mfinal.go

type finalizer struct { fn *funcval // function to call arg unsafe.Pointer // ptr to object nret uintptr // bytes of return values from fn fint *_type // type of first argument of fn ot *ptrtype // type of ptr to object } type finblock struct { alllink finblock next finblock cnt int32 _ int32 fin [(_FinBlockSize - 2ptrSize - 24) / unsafe.Sizeof(finalizer{})]finalizer }

另有几个全局变量用来管理finblock。

mfinal.go

var finq *finblock // 待执行 finalizer 队列(链表) var finc *finblock // 提供 finblock 缓存复用对象 var allfin *finblock // 所有 finblock 列表

向队列添加析构函数的过程,基本上就是对finblock的操作。每次都向待执行队列的第一个容器finq添加,直到装满后将finq换成新的finblock块。

mfinal.go

func queuefinalizer(p unsafe.Pointer, fn *funcval, nret uintptr, fint *_type, ot *ptrtype) { // finq 是链表的第一个 finblock,也是当前操作的目标 // 如果为空,或者内部数组已满,则重新获取 finblock 替换 finq if finq nil || finq.cnt int32(len(finq.fin)) { // 如果复用缓存已空,申请新内存 if finc == nil { // 有关 persistent,请参考内存分配相关章节 finc = (*finblock)(persistentalloc(_FinBlockSize, 0, &memstats.gc_sys)) // 添加到 allfin 链表 finc.alllink = allfin allfin = finc } // 从复用缓存头部提取 finblock,并调整 finc 链表 block := finc finc = block.next // 将新 finblock 挂到 finq 待执行队列头部 block.next = finq finq = block } // finq.cnt 记录了 finblock 内部数组使用位置索引 f := &finq.fin[finq.cnt] finq.cnt++ // 设置相关属性 f.fn = fn f.nret = nret f.fint = fint f.ot = ot f.arg = p // 设置 fing 唤醒标志 fingwake = true }

执行队列准备好以后,须由专门的fing goroutine负责执行。在SetFinalizer里我们就看到过createfing函数调用。

mfinal.go

func createfing() { // 确保仅执行一次 if fingCreate == 0 && cas(&fingCreate, 0, 1) { go runfinq() } }

mfinal.go

var fing *g // goroutine that runs finalizers var fingwait bool // 休眠标记 var fingwake bool // 唤醒标记 func runfinq() { for { // 置换运行队列 // 因为是并发,所以在执行 runfinq 时不能影响新的添加操作 fb := finq finq = nil // 如果队列为空,则进入休眠 if fb == nil { // 设置全局变量 fing gp := getg() fing = gp // 设置休眠标志,休眠 fingwait = true goparkunlock(&finlock, “finalizer wait”, traceEvGoBlock, 1) // 唤醒后重新检查队列 continue } // 遍历 finq 链表 for fb != nil { // 遍历 finblock 内部数组 for i := fb.cnt; i > 0; i— { // 获取并执行 finalizer f := (*finalizer)(add(unsafe.Pointer(&fb.fin), …)) reflectcall(nil, unsafe.Pointer(f.fn), frame, …) f.fn = nil f.arg = nil f.ot = nil fb.cnt = i - 1 } next := fb.next // 将当前已完成任务的 finalizer 对象放回 finc 复用缓存 fb.next = finc finc = fb fb = next } } }

一路走来,已记不清runtime创建了多少类似fing这样的以死循环方式工作的goroutine了。好在像fing这样的都是按需创建的。

循环遍历所有finblock,执行其中的析构函数。要说有所不同,就是它们会在同一个G栈串行执行。剩余问题是,当fing执行完毕进入休眠后,由谁来唤醒?要知道queuefinalizer仅仅设置了fingwake标志。

还记得调度循环里四处查找可用任务的findrunnable函数吗?没错,fing也是它要寻找的目标之一。

proc1.go

func findrunnable() (gp *g, inheritTime bool) { // 如果 fing 正在休眠,且被设置了唤醒标志 if fingwait && fingwake { // 唤醒 if gp := wakefing(); gp != nil { ready(gp, 0) } } }

mfinal.go

func wakefing() *g { var res *g // 再次检查唤醒条件 if fingwait && fingwake { fingwait = false fingwake = false res = fing } return res }

不管是panic,还是finalizer,都有特定的使用场景,因为它们有相应的设计制约。这种制约不应被看作是缺陷,毕竟我们本就不该让它们去做无法保证的事情。保持有限度的谨慎和悲观不是坏事,但不能因此就无理由地去抵制和忽视。了解其原理,永远不要停留在文档的字里行间。